package top.fullj.chase.internal;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;

import java.util.Iterator;
import java.util.ServiceLoader;

import top.fullj.chase.spi.VirtLog;

@Aspect
public class Chase {

    private static final VirtLog LOG;

    static {
        Iterator<VirtLog> it = ServiceLoader.load(VirtLog.class).iterator();
        LOG = it.hasNext() ? it.next() : new ConsoleLog();
    }

    private static volatile boolean enabled = true;

    public static void setEnabled(boolean enabled) {
        Chase.enabled = enabled;
    }

    @Pointcut("within(@top.fullj.chase.annotation.DebugLog *)")
    public void withinAnnotatedClass() {}

    @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
    public void methodInsideAnnotatedType() {}

    @Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
    public void constructorInsideAnnotatedType() {}

    @Pointcut("execution(@top.fullj.chase.annotation.DebugLog * *(..)) || methodInsideAnnotatedType()")
    public void method() {}

    @Pointcut("execution(@top.fullj.chase.annotation.DebugLog *.new(..)) || constructorInsideAnnotatedType()")
    public void constructor() {}

    @Around("method() || constructor()")
    public Object intercept(ProceedingJoinPoint pjp) throws Throwable {
        if (!enabled) {
            return pjp.proceed();
        }

        methodEnter(pjp);
        Timer timer = new Timer();
        try {
            Object result = pjp.proceed();
            methodReturn(pjp, result, timer.millis());
            return result;
        } catch (Throwable tr) {
            methodThrown(pjp, tr, timer.millis());
            throw tr;
        }
    }

    private static void methodEnter(JoinPoint pjp) {
        CodeSignature codeSignature = (CodeSignature) pjp.getSignature();

        String[] parameterNames = codeSignature.getParameterNames();
        Object[] parameterValues = pjp.getArgs();

        StringBuilder sb = new StringBuilder("==> ");
        sb.append(asMethodQualifier(codeSignature)).append('(');
        for (int i = 0; i < parameterValues.length; i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(parameterNames[i]).append('=');
            sb.append(Strings.toString(parameterValues[i]));
        }
        sb.append(')');

        LOG.enter(sb.toString());
    }

    private static void methodReturn(JoinPoint pjp, Object result, long millis) {
        Signature signature = pjp.getSignature();

        boolean hasReturnType = signature instanceof MethodSignature
                && ((MethodSignature) signature).getReturnType() != void.class;

        StringBuilder sb = new StringBuilder("<== ")
                .append(asMethodQualifier(signature))
                .append(" [").append(millis).append("ms]");

        if (hasReturnType) {
            sb.append(" = ");
            sb.append(Strings.toString(result));
        }

        LOG.ret(sb.toString());
    }

    @SuppressWarnings("StringBufferReplaceableByString")
    private static void methodThrown(JoinPoint pjp, Throwable tr, long millis) {
        StringBuilder sb = new StringBuilder("X== ")
                .append(asMethodQualifier(pjp.getSignature()))
                .append(" [").append(millis).append("ms] ")
                .append("THROWN");

        LOG.thrown(sb.toString(), tr);
    }

    private static String asMethodQualifier(Signature signature) {
        Class<?> cls = signature.getDeclaringType();
        return asTypeName(cls) + "." + signature.getName();
    }

    private static String asTypeName(Class<?> cls) {
        Class<?> enclosingCls = cls.getEnclosingClass();
        if (enclosingCls == null) {
            return cls.getSimpleName();
        }
        return asTypeName(enclosingCls) + "$" + cls.getSimpleName();
    }

}
